Write the main boot sector code

📄 返回目录页

本次代码:

         ;代码清单5-1 
         ;文件名:c05_mbr.asm
         ;文件说明:硬盘主引导扇区代码
         ;创建日期:2011-3-31 21:15 
         
         mov ax,0xb800                 ;指向文本模式的显示缓冲区
         mov es,ax

         ;以下显示字符串"Label offset:"
         mov byte [es:0x00],'L'
         mov byte [es:0x01],0x07
         mov byte [es:0x02],'a'
         mov byte [es:0x03],0x07
         mov byte [es:0x04],'b'
         mov byte [es:0x05],0x07
         mov byte [es:0x06],'e'
         mov byte [es:0x07],0x07
         mov byte [es:0x08],'l'
         mov byte [es:0x09],0x07
         mov byte [es:0x0a],' '
         mov byte [es:0x0b],0x07
         mov byte [es:0x0c],"o"
         mov byte [es:0x0d],0x07
         mov byte [es:0x0e],'f'
         mov byte [es:0x0f],0x07
         mov byte [es:0x10],'f'
         mov byte [es:0x11],0x07
         mov byte [es:0x12],'s'
         mov byte [es:0x13],0x07
         mov byte [es:0x14],'e'
         mov byte [es:0x15],0x07
         mov byte [es:0x16],'t'
         mov byte [es:0x17],0x07
         mov byte [es:0x18],':'
         mov byte [es:0x19],0x07

         mov ax,number                 ;取得标号number的偏移地址
         mov bx,10

         ;设置数据段的基地址
         mov cx,cs
         mov ds,cx

         ;求个位上的数字
         mov dx,0
         div bx
         mov [0x7c00+number+0x00],dl   ;保存个位上的数字

         ;求十位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x01],dl   ;保存十位上的数字

         ;求百位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x02],dl   ;保存百位上的数字

         ;求千位上的数字
         xor dx,dx
         div bx
         mov [0x7c00+number+0x03],dl   ;保存千位上的数字

         ;求万位上的数字 
         xor dx,dx
         div bx
         mov [0x7c00+number+0x04],dl   ;保存万位上的数字

         ;以下用十进制显示标号的偏移地址
         mov al,[0x7c00+number+0x04]
         add al,0x30
         mov [es:0x1a],al
         mov byte [es:0x1b],0x04
         
         mov al,[0x7c00+number+0x03]
         add al,0x30
         mov [es:0x1c],al
         mov byte [es:0x1d],0x04
         
         mov al,[0x7c00+number+0x02]
         add al,0x30
         mov [es:0x1e],al
         mov byte [es:0x1f],0x04

         mov al,[0x7c00+number+0x01]
         add al,0x30
         mov [es:0x20],al
         mov byte [es:0x21],0x04

         mov al,[0x7c00+number+0x00]
         add al,0x30
         mov [es:0x22],al
         mov byte [es:0x23],0x04
         
         mov byte [es:0x24],'D'
         mov byte [es:0x25],0x07
          
   infi: jmp near infi                 ;无限循环
      
  number db 0,0,0,0,0
  
  times 203 db 0
            db 0x55,0xaa

主引导扇区(Main Boot Sector,MBR)。硬盘的0面0头1扇区

一个有效的主引导扇区,其最后2字节应当是0x55和0xAA。ROM-BIOS程序首先检测这两个标志,如果主引导扇区有效,则以一个段间转移指令jmp 0x0000:0x7c00跳到那里继续执行。

一般来说,主引导扇区是由操作系统负责的。正常情况下,一段精心编写的主引导扇区代码将检测用来启动计算机的操作系统,并计算出它所在的硬盘位置。然后,它把操作系统的自举代码加载到内存,也用jmp指令跳转到那里继续执行,直到操作系统完全启动。

?

计算机是怎么在屏幕上显示文字的?

显卡和显存

显示需要显示器和显卡,显卡给显示器提供内容,同时控制显示器的显示模式和状态,
显示器则将内容显示在屏幕上。

显卡控制显示器的最小单位是像素,一个像素对应着屏幕上的一个点。显卡都有自己的存储器,因为它位于显卡上,故称显示存储器(Video RAM, VRAM),简称显存

显示器显示黑白图像只需要控制每个像素是亮,还是不亮。不亮当成比特“0”,亮看成比特“1”。
Pasted image 20241201221253.png
显存的第1字节对应着屏幕左上角连续的8个像素;第2字节对应着屏幕上后续的8个像素,后面的依次类推。

显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。

黑白两个颜色只需要1比特,而24比特可以对应16777216种颜色。
上述显示器的是图形模式;

显示文字的当然是文本模式,对于显示器来说图像和文字都是二进制数

显存可以存放字符的代码,第1个代码对应着屏幕左上角第1个字符,剩下的工作是如何用代码来控制屏幕上的像素,使它们或明或暗以构成字符的轮廓,这是字符发生器和控制电路的事情。
Pasted image 20241202173900.png

为了方便访问显存,显存被映射在了内存空间里,8086可访问的内存空间中除了内存和ROM-BIOS,还有320KB,即0xA0000~0xEFFFF,这段地址空间由特定的外围设备来提供,其中就包括显卡。

文本模式下显存到内存的映射:
Pasted image 20241202200748.png
一直以来,0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。

初始化段寄存器

文本模式显存是从0xB8000开始的,操作这一段物理地址时,段地址为0xB800,所以要将DS Segment-register的值改为0xB800,但也不一定要用DS也可以用ES(附加段寄存器)。
更改段寄存器的值:

mov ax,0xB800
mov es,ax

INTEL处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:

mov 段寄存器,通用寄存器 
mov 段寄存器,内存单元

显存的访问和ASCII代码

给段寄存器指向0xB800的值后,现在的逻辑地址可以正常地对应屏幕左上角的字符。
字符也是一个二进制代码,和正常计算的数字没有区别,只是看不同的硬件有不同的解释。所以1967年,美国国家标准学会制定了美国信息交换标准代码(American Standard Code for InformationInterchange, ASCII)
Pasted image 20241202204947.png
ASCII表中有相当一部分代码是不可打印和显示的,它们用于控制通信过程。比如,LF是换行;CR是回车;DEL和BS分别是删除和退格,在我们平时用的键盘上也是有的;BEL是振铃(使远方的终端响铃,以引起注意);SOH是文头;EOT是文尾;ACK是确认。

屏幕上的每个字符对应着显存中连续2字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。
字符代码及字符属性:
Pasted image 20241202210134.png
字符的显示属性(1字节)分为两部分,低4位定义的是前景色,高4位定义的是背景色。
色彩主要由R、G、B这3位决定。K是闪烁位,为0时不闪烁,为1时闪烁;I是亮度位,为0时正常亮度,为1时呈高亮。
80×25文本模式下的颜色表:
Pasted image 20241202210514.png

在屏幕上显示文字

显示字符

从源程序的第10行开始,到第35行,目的是显示一串字符“Label offset:”

在第10行中:

mov byte [es:0x00],'L'

’L‘的ASCII码是0x4C,也就可以这样说:

mov byte [es:0x00],0x4C

关键字“byte”用来修饰目的操作数,指出本次传送是以字节的方式进行的。

[es:0x00] 是内存地址,0x00是偏移地址。如果是不指定寄存器es来访问内存会默认用段寄存器ds,也就是这样写[0x00]。

在16位的处理器上,一次可以操作的数据宽度是8位,也可以是16位,所以这里0x4C和0x00
可以是0x4C,0x00,也可以是0x004C,0x0000。我们这里用了“byte”修饰符,所以是按照字节的方式进行的,如果不使用编译器不知道你的意图只可以报错。操作字单元可以用“word”进行修饰。
下面的指令就不需要任何修饰:

mov [0x00],al    ; 按字节操作
mov ax,[0x02]    ; 按字操作

显示标号的汇编地址

标号

在源程序的编译阶段,编译器会把代码整体上作为一个独立的段来处理,并从0开始计算和跟踪每条指令的地址。因为该地址是在编译期间计算的,故称为汇编地址。汇编地址是在源程序编译期间,编译器为每条指令确定的汇编位置(Assembly Position),指示该指令相对于程序或者段起始处的距离,以字节计。当编译后的程序装入物理内存后,它又是该指令在内存段内的偏移地址。

汇编程序经过编译后除了.bin文件,还会有一个.lst的列表文件
本文上面的代码编译后列表文件的内容:

     1                                           ;代码清单5-1 
     2                                           ;文件名:c05_mbr.asm
     3                                           ;文件说明:硬盘主引导扇区代码
     4                                           ;创建日期:2011-3-31 21:15 
     5                                           
     6 00000000 B800B8                           mov ax,0xb800                 ;指向文本模式的显示缓冲区
     7 00000003 8EC0                             mov es,ax
     8                                  
     9                                           ;以下显示字符串"Label offset:"
    10 00000005 26C60600004C                     mov byte [es:0x00],'L'
    11 0000000B 26C606010007                     mov byte [es:0x01],0x07
    12 00000011 26C606020061                     mov byte [es:0x02],'a'
    13 00000017 26C606030007                     mov byte [es:0x03],0x07
    14 0000001D 26C606040062                     mov byte [es:0x04],'b'
    15 00000023 26C606050007                     mov byte [es:0x05],0x07
    16 00000029 26C606060065                     mov byte [es:0x06],'e'
    17 0000002F 26C606070007                     mov byte [es:0x07],0x07
    18 00000035 26C60608006C                     mov byte [es:0x08],'l'
    19 0000003B 26C606090007                     mov byte [es:0x09],0x07
    20 00000041 26C6060A0020                     mov byte [es:0x0a],' '
    21 00000047 26C6060B0007                     mov byte [es:0x0b],0x07
    22 0000004D 26C6060C006F                     mov byte [es:0x0c],"o"
    23 00000053 26C6060D0007                     mov byte [es:0x0d],0x07
    24 00000059 26C6060E0066                     mov byte [es:0x0e],'f'
    25 0000005F 26C6060F0007                     mov byte [es:0x0f],0x07
    26 00000065 26C606100066                     mov byte [es:0x10],'f'
    27 0000006B 26C606110007                     mov byte [es:0x11],0x07
    28 00000071 26C606120073                     mov byte [es:0x12],'s'
    29 00000077 26C606130007                     mov byte [es:0x13],0x07
    30 0000007D 26C606140065                     mov byte [es:0x14],'e'
    31 00000083 26C606150007                     mov byte [es:0x15],0x07
    32 00000089 26C606160074                     mov byte [es:0x16],'t'
    33 0000008F 26C606170007                     mov byte [es:0x17],0x07
    34 00000095 26C60618003A                     mov byte [es:0x18],':'
    35 0000009B 26C606190007                     mov byte [es:0x19],0x07
    36                                  
    37 000000A1 B8[2E01]                         mov ax,number                 ;取得标号number的偏移地址
    38 000000A4 BB0A00                           mov bx,10
    39                                  
    40                                           ;设置数据段的基地址
    41 000000A7 8CC9                             mov cx,cs
    42 000000A9 8ED9                             mov ds,cx
    43                                  
    44                                           ;求个位上的数字
    45 000000AB BA0000                           mov dx,0
    46 000000AE F7F3                             div bx
    47 000000B0 8816[2E7D]                       mov [0x7c00+number+0x00],dl   ;保存个位上的数字
    48                                  
    49                                           ;求十位上的数字
    50 000000B4 31D2                             xor dx,dx
    51 000000B6 F7F3                             div bx
    52 000000B8 8816[2F7D]                       mov [0x7c00+number+0x01],dl   ;保存十位上的数字
    53                                  
    54                                           ;求百位上的数字
    55 000000BC 31D2                             xor dx,dx
    56 000000BE F7F3                             div bx
    57 000000C0 8816[307D]                       mov [0x7c00+number+0x02],dl   ;保存百位上的数字
    58                                  
    59                                           ;求千位上的数字
    60 000000C4 31D2                             xor dx,dx
    61 000000C6 F7F3                             div bx
    62 000000C8 8816[317D]                       mov [0x7c00+number+0x03],dl   ;保存千位上的数字
    63                                  
    64                                           ;求万位上的数字 
    65 000000CC 31D2                             xor dx,dx
    66 000000CE F7F3                             div bx
    67 000000D0 8816[327D]                       mov [0x7c00+number+0x04],dl   ;保存万位上的数字
    68                                  
    69                                           ;以下用十进制显示标号的偏移地址
    70 000000D4 A0[327D]                         mov al,[0x7c00+number+0x04]
    71 000000D7 0430                             add al,0x30
    72 000000D9 26A21A00                         mov [es:0x1a],al
    73 000000DD 26C6061B0004                     mov byte [es:0x1b],0x04
    74                                           
    75 000000E3 A0[317D]                         mov al,[0x7c00+number+0x03]
    76 000000E6 0430                             add al,0x30
    77 000000E8 26A21C00                         mov [es:0x1c],al
    78 000000EC 26C6061D0004                     mov byte [es:0x1d],0x04
    79                                           
    80 000000F2 A0[307D]                         mov al,[0x7c00+number+0x02]
    81 000000F5 0430                             add al,0x30
    82 000000F7 26A21E00                         mov [es:0x1e],al
    83 000000FB 26C6061F0004                     mov byte [es:0x1f],0x04
    84                                  
    85 00000101 A0[2F7D]                         mov al,[0x7c00+number+0x01]
    86 00000104 0430                             add al,0x30
    87 00000106 26A22000                         mov [es:0x20],al
    88 0000010A 26C606210004                     mov byte [es:0x21],0x04
    89                                  
    90 00000110 A0[2E7D]                         mov al,[0x7c00+number+0x00]
    91 00000113 0430                             add al,0x30
    92 00000115 26A22200                         mov [es:0x22],al
    93 00000119 26C606230004                     mov byte [es:0x23],0x04
    94                                           
    95 0000011F 26C606240044                     mov byte [es:0x24],'D'
    96 00000125 26C606250007                     mov byte [es:0x25],0x07
    97                                            
    98 0000012B E9FDFF                     infi: jmp near infi                 ;无限循环
    99                                        
   100 0000012E 0000000000                number db 0,0,0,0,0
   101                                    
   102 00000133 00<rep CBh>               times 203 db 0
   103 000001FE 55AA                                db 0x55,0xaa

第一条指令mov ax,0xb800的汇编地址是0x00000000,对应的机器代码为B8 00 B8;

在代码的第98行中“infi” 是一个标号,也就是说它代表着汇编地址0000012B。
那个冒号是无所谓的。

Info

在NASM汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令的汇编地址。

如何显示十进制数字

在程序的第37行中,number是一个标号,是100行的0000012E。也就是这条代码相当于:

mov ax,0x012E

其中字操作数是10进制的302,当然直接把ax的内容给到显示缓冲区是无法显示字符“302”的。
字符“0”的ASCII代码是0x30,字符“1”的ASCII代码是0x31,字符“9”的ASCII代码是0x39。这就是说,把每次相除得到的余数加上0x30,在屏幕上显示就没问题了。
所以把302这个数拆开来一个一个加上0x30,再显示在屏幕上就好了。

在程序中声明并初始化数据

可以用处理器提供的除法指令来分解一个数的各个数位,但是每次除法操作后得到的数位需要临时保存起来以备后用。最好的办法是在内存中专门留出一些空间来保存这些数位。
在第100行的number中代表了后面的代码:

number db 0,0,0,0,0
;用于声明并初始化这些数据,而标号number则代表了这些数据的起始汇编地址。
;就是这些初始化的数是从0000012E开始的。

要放在程序中的数据是用DB指令来声明(Declare)的,DB的意思是声明字节(Declare Byte),所以,跟在它后面的操作数都占一字节的长度(位置)。

注意

如果要声明超过一个以上的数据,各个操作数之间必须以逗号隔开。
声明的数据可以是任何值,只要不超过伪指令所指示的大小。

除此之外,DW(Declare Word)用于声明字数据,DD(Declare DoubleWord)用于声明双字(两个字)数据,DQ(Declare Quad Word)用于声明四字数据。DB、DW、DD和DQ并不是处理器指令,它只是编译器提供的汇编指令,所以称作伪指令(Pseudo Instruction)。
伪指令是汇编指令的一种,它没有对应的机器指令,所以它不是机器指令的助记符,仅仅在编译阶段由编译器执行,编译成功后,伪指令就消失了。所以在程序执行时,伪指令是得不到处理器光顾的。实际上,程序执行时,伪指令已不存在。
像这样一段代码:

00000000 diva dw 0x08f9
00000002 divb db 0x08
00000003 mov ax, [diva]
00000006 div byte [divb]

在编译阶段,编译器在生成这两条指令的机器码之前,会先将它们转换成以下的形式:

mov ax, [0x0000]
div byte [0x0002]

处理器可以访问任何内存位置,但不一定每个位置都是空闲的。伪指令DB用来保留只供自己访问的内存位置。

分解数的各个数位

要分解一个数的各个数位,需要做除法。8086处理器提供了除法指令div,它可以做两种类型的除法。
#第一种类型是用16位的二进制数除以8位的二进制数。
比如:

div cl
div byte [0x0023]

被除数是放在ax寄存器里的,这是必要的。指令执行后,商在寄存器al中,余数在寄存器ah中。
第一条指令是将ax与cl的内容相除,而除数放在cl中。
第二条指令是去逻辑地址 [ds:0x0023]形成的物理地址中取出一个字节的内容当除数。

Info

我们通常是不知道数据的具体位置的,所以关于地址的部分大部分多是以标号的形式表示的。

#第二种类型是用32位的二进制数除以16位的二进制数。
当然16位的处理器是不可能直接提供32位的数的,所以要将32位的数拆开来,把高16位放在dx寄存器,把低16位放在ax寄存器里。
除数还是和第一种一样,两个数相除后商在ax中,余数在dx中。

相除之后,我们要从寄存器里取出余数,将余数保存在数据段。我们在100行中初始化了5个字节数据,这里的余数就传送到那里。
就像代码的第47行:

mov [0x7c00+number+0x00],dl   ;保存个位上的数字

这里将dl的内容传送到了内存地址[0x7c00+number+0x00]中,也就是[0x7D2E]。
我们知道程序是从一个段开始运行的,所以number标号是相对于程序开始处的汇编地址,
这个number在[]中用也是一个普通的值。而number代表的值与cs的内容相加应该我们的目标地址。但这里的代码比较特殊,这是主引导程序,是从0x0000:0x7c00开始的,代码段和数据段都从0x00000开始,而程序从0x7c000开始。所以,number要与0x7c00相加才可以得到我们存放数据的起始位置。0x00就很简单了,第一个数据。
Pasted image 20241207195501.png
xor指令在数字逻辑里是异或(eXclusive OR)的意思,或者叫互斥或、互斥的或运算。
在数字逻辑里,如果0代表假,1代表真 所以:

0 xor 0 = 0
0 xor 1 = 1
1 xor 0 = 1
1 xor 1 = 0

xor指令的目的操作数可以是通用寄存器和内存单元,源操作数可以是通用寄存器、内存单元和立即数(不允许两个操作数同时为内存单元)。
一般地,xor指令的两个操作数应当具有相同的数据宽度。
总之我们知道了两个操作数相同为假
执行xor dx dx后会给dx寄存器清零。
清零后,我们就可以计算后面的数位了,ax的值也就又成为了被除数,因为是32为除16位,ax和dx是联合的,所以才有了前面的操作。

无限循环

一直到程序的96行这个程序的任务已经结束了,处理器会继续向下取指令执行,执行到非指令的区域就不好了。鉴于我们任务结束了,也没活了,同时避免问题,在98行整了个无限循环:

infi: jmp near infi

jmp是转移指令,用于使处理器脱离当前的执行序列,转移到指定的地方执行,关键字near表示目标位置依然在当前代码段内。
这一段是jmp的一种用法叫做相对近转移。jmp后面的是标号计算这种;
在编译阶段,编译器会将标号处的汇编地址减去下一条指令的地址(也就是当前ip寄存器的值),得到操作数。而这个操作数再和ip寄存器的值相加,以near取值的低16位,就得到了目标指令的汇编地址,也就是当前的位置。

完成并编译主引导扇区代码

运行主引导扇区的代码系统启动,但主引导扇区里的程序是错误的或者什么都没有,无效的,其结果是陷入宕机状态。所以一个有效的主引导扇区,其最后2字节的数据必须是0x55和0xAA。否则,这个扇区里保存的就不是一些有意而为的数据,系统就会尝试以光盘和U盘启动。
定义这两个字节就用db伪指令就可以做到:

db 0x55,0xaa

但是这两个字节要在512字节中的最后,而我们无法确定我们这里代码的长度,现在的指令在内存的哪里也不知道。
#我们当然有非常好的办法解决,但还不宜在这里说明。
我们知道,在前面的内容和结尾的0xAA55之间,有203字节的空洞。因此,源程序的第102行,用于声明203个为0的数值来填补。
所以用了times伪指令,可用于重复它后面的指令若干次。

102 00000133 00<rep CBh>               times 203 db 0

db 0重复了203次。

然后再将编译好的裸二进制文件写入到虚拟硬盘里,在虚拟机执行。
最后的结果:
Pasted image 20241208162956.png